/* 
 *  Copyright 2012 CodeMagi, Inc.
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.codemagi.servlets;

import com.codemagi.login.AccessException;
import com.codemagi.login.LoginException;
import com.codemagi.login.model.IUser;
import com.codemagi.servlets.model.Securable;
import com.codemagi.util.*;
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.apache.log4j.*;
import org.exolab.castor.jdo.Database;
import org.exolab.castor.jdo.ObjectNotFoundException;
import org.exolab.castor.jdo.PersistenceException;
import org.exolab.castor.mapping.MappingException;
import org.exolab.castor.xml.XMLException;
import org.json.*;

/**
 * NavigationController extends BaseServlet to provide a standardized handler for 
 * REST-style access to Objects. This servlet processes URLs in the form of: 
 * 
 * http://mysite.com/object/id [/xml[/stylesheet.xsl]] | [/page.jsp]
 *
 * Where:
 * 
 * object         = The object type to get
 * id             = The numeric ID of the object you wish to retrieve
 * xml            = Optionally convert the object into XML
 * stylesheet.xsl = Optionally format the XML using the provided XSL stylesheet 
 * page.jsp       = Optionally add the object to the request attributes and forward to the provided JSP page
 *
 *  @version 1.0
 *  @author August Detlefsen for CodeMagi, Inc.
 */
public class NavigationController extends com.codemagi.servlets.BaseServlet {

    Logger log = Logger.getLogger(this.getClass());

    //MESSAGES specific to this controller
    protected static final String MSG_OBJECT_NOT_FOUND = "The requested object does not exist.";

    /**
     * The type of object to return for each call
     */
    protected HashMap typeClasses = new HashMap<String,Class>();

    /**
     * The default view to forward to after loading the object
     */
    protected HashMap typeViews   = new HashMap<String,String>();

    /**
     * The name to use when placing the object in the request attributes
     */
    protected HashMap typeNames   = new HashMap<String,String>();

    /**
     * The name to use when placing the object's ID in the session attributes
     */
    protected HashMap typeIdNames   = new HashMap<String,String>();

    /**
     * The name to use when placing the object in the session attributes
     */
    protected HashMap sessionNames   = new HashMap<String,String>();

    /**
     * Whether to return a default (blank) version of the object if none is found in teh database
     */
    protected HashMap<String,Boolean> typeDefaults = new HashMap<String,Boolean>();


    public void service(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {

	String nextPage = VIEW_MAIN;

	//get the command passed in from the URL
	String command      = request.getPathInfo();

	log.debug("COMMAND: " + command);
	log.debug("URI: " + request.getRequestURI());
	log.debug("URL: " + request.getRequestURL());
	log.debug("SERVLET PATH: " + request.getServletPath());

	Stopwatch timer = new Stopwatch();
	timer.start();

	try {
	    //dispatch actions based on request
	    if ( !Utils.isEmpty(command) ) {
		nextPage = dispatchCommand(request, response);    
	    }

        } catch (AccessException ae) {
	    nextPage = handleAccessException(request, response, ae);

        } catch (LoginException le) {
	    nextPage = handleLoginException(request, response, le);

        }

	//show timing
	timer.stop();
	log.debug("Command complete in: " + timer.getElapsedTime());

        //forward on
        log.debug("NavigationController FORWARDING TO: " + nextPage);

	if (!VIEW_NONE.equals(nextPage)) {
	    RequestDispatcher dispatcher = getServletContext().getRequestDispatcher(nextPage);
	    dispatcher.forward(request, response);
	}

        return;
    }


    //DISPATCHERS ----------------------------------------------------------------------


    /**
     * Dispatches navigation commands. 
     *
     * 1) Parses the request URL to determine the type and ID of Object to load
     * 2) Loads the object from DB and places it in the request attributes
     * 3) Forwards to either the default view or another page if specified on the URL
     *
     * NOTE: If an object of the correct type and ID already exists in the request, 
     *       it will be used instead of reloading it from the database!
     * 
     * @see https://www.openeco.org/api For information on how the URL is parsed
     */
    protected String dispatchCommand(HttpServletRequest request, HttpServletResponse response) 
	throws LoginException, AccessException {

	String nextPage = VIEW_MAIN;

        Database db = null;

	Stopwatch timer = new Stopwatch();
	timer.start();
        
        try {
            //get the page ID from the URL
	    String command   = request.getPathInfo();
	    command          = command.substring(1);  //trim the leading slash                               

            String[] split   = command.split("/", 2);
            Integer objectId = convertInteger( split[0] ); //will throw exception if unparseable
	    log.debug("ID: " + objectId);
	    
            String ext       = (split.length > 1) ? "/" + split[1] : "";

	    //determine the proper object to load
	    String type = request.getServletPath();
	    type        = type.substring(1);  //trim the leading slash                           
	    log.debug("TYPE: " + type);
	    
	    //What is the request name for this Object?
	    String typeName = (String)typeNames.get(type);
	    log.debug("NAME: " + typeName);
	    
	    //What is the class type for this Object?
	    Class objectClass = (Class)typeClasses.get(type);
	    log.debug("CLASS: " + objectClass);
	    
	    //get the object from request if it already exists
	    Object obj = request.getAttribute(typeName);
	    log.debug("REQUEST OBJ: " + obj);

	    if (obj == null || !objectClass.isInstance(obj)) {

		log.debug("   Loading from database...");
		
		//otherwise, load it from the database
		db = getJDODatabase();
		db.begin();
		
		try {
		    obj = db.load(objectClass, objectId, Database.ReadOnly);
		    //	    Node obj = getNode(objectId, 1, db);
		    log.debug("DB OBJ: " + obj);
		} catch (ObjectNotFoundException onfe) {

		    Boolean sendDefault = typeDefaults.get(type);

		    if (sendDefault != null && sendDefault) {
			obj = objectClass.newInstance();
			((com.codemagi.servlets.model.BaseBean)obj).setId(objectId);
		    } else {
			throw new AppException(MSG_OBJECT_NOT_FOUND);
		    }
		}
		
		db.rollback();
		timer.lap();
		log.debug("      db load complete in: " + timer.getLapTime());
	    }
	    
	    //if necessary, check for permissions to access this object
	    if (obj instanceof Securable) {

		log.debug("   Securable check...");
		
		//		IUser user = checkLogin(request, request.getRequestURI());
		IUser user = getUser(request);
		
		if (!((Securable)obj).hasUser(user)) {
		    if (user == null || !user.isAuthenticated()) {
			//probably failed security check due to not being logged in, give em another chance
			request.setAttribute("nextPage", request.getRequestURI());
			throw new LoginException();
		    }

		    //logged in and still failed 
		    throw new AccessException();
		}

		timer.lap();
		log.debug("      securable check complete in: " + timer.getLapTime());
	    }
	    
	    //set the object into request
	    request.setAttribute(typeName, obj);
	    
	    //set the object's Id into the session
	    //yes, this is necessary in some cases
	    String typeIdName = (String)typeIdNames.get(type);
	    if ( !Utils.isEmpty(typeIdName) ) request.getSession().setAttribute(typeIdName, objectId);

	    //set the object into session 
	    //yes, this is necessary in some cases, but use sparingly
	    String sessionName = (String)sessionNames.get(type);
            if ( !Utils.isEmpty(sessionName) ) request.getSession().setAttribute(sessionName, obj);

	    //make sure they are not trying to access resources in WEB-INF
	    if ( ext.contains("WEB-INF") ) throw new AccessException("Attempt to access resources in WEB-INF");

	    //determine the next page to route to
	    log.debug("about to call getNextPage(): nextPage: " + nextPage);
	    nextPage = getNextPage(type, ext, obj, request, response); 
	    log.debug("        after getNextPage(): nextPage: " + nextPage);
	
	    //execute the after command hook
	    doAfterCommand(obj, request, response);

	} catch (AppException ae) {
	    log.debug("", ae);
            
	} catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);
            
	} catch (InstantiationException ie) {
	    log.debug("", ie);

	} catch (IllegalAccessException ie) {
	    log.debug("", ie);

        } finally { 
            closeJDODatabase(db);

        }

	return nextPage;
    }

    /**
     * Determines the next page to route a request based on the URL and the type of object. 
     * Override this method to implement object-specific forwarding (such as with NavPage)
     */ 
    protected String getNextPage(String type, String ext, Object obj, HttpServletRequest request, HttpServletResponse response) {
	String nextPage = VIEW_MAIN;

	if (!Utils.isEmpty(ext)) {
	    
	    if ( ext.startsWith("/xml") ) {
		//return XML
		nextPage = returnXml(obj, ext, response);
		
	    } else if ( ext.startsWith("/json") ) {
		//return JSON
		nextPage = returnJson(obj, request, response);
		
	    } else {
		nextPage = ext;
	    }
	    
	} else {
	    String typeView = (String)typeViews.get(type);
	    if (!Utils.isEmpty(typeView)) nextPage = typeView;
	}

	return nextPage;
    }


    protected String returnXml(Object obj, String ext, HttpServletResponse response) {

	log.debug("returnXml( " + obj + ", " + ext + ", response )");

	String command = ext.substring(1);  //trim the leading slash
	String[] split = command.split("/", 2);
	log.debug("SPLIT: " + CollectionUtils.commaDelimit(split) );

	if (split.length < 1) return ext;
	log.debug("1");

	if ( !"xml".equals(split[0]) ) return ext;
	log.debug("2");

	//writer to output response
	PrintWriter writer = null;

	try {

	    Stopwatch timer = new Stopwatch();
	    timer.start();
	    
	    String xml    = marshal(obj, mapping);
	    String output = "";

	    timer.lap();
	    log.debug("XML Marshalling complete in: " + timer.getLapTime());

	    //prevent browser caching this XML
	    response.setHeader("Cache-Control","no-store");  //HTTP 1.1   
	    response.setHeader("Pragma","no-cache"); //HTTP 1.0

	    //prevents caching at the proxy server     
	    response.setDateHeader("Expires", -1);  

	    if ( split.length > 1 && !Utils.isEmpty(split[1]) && split[1].contains(".xsl") ) {
		//stylesheet specified
		log.debug("stylesheet: " + split[1]);
		
		output = transformXml(xml, "/" + split[1]);
		response.setContentType("text/plain");

		timer.lap();
		log.debug("XSL Transformation complete in: " + timer.getLapTime());
		
	    } else {
		//just use the raw XML
		log.debug("returning raw XML");

		output = xml;
		
	    }

	    //send output to the browser
	    log.debug("OUTPUT: " + output);
	    writer = response.getWriter();
	    writer.write(output);
	    writer.flush();

	    timer.stop();
	    log.debug("returnXML complete in: " + timer.getElapsedTime());

	    return VIEW_NONE;

	} catch (IOException ioe) {
	    log.debug("", ioe);

	} catch (MappingException me) {
	    log.debug("", me);
            
	} catch (XMLException xe) {
	    log.debug("", xe);

	} finally {
	    closeStream(writer);

	}

	return ext;
    }


    protected String returnJson(Object obj, HttpServletRequest request, HttpServletResponse response) {

	log.debug("returnJson( " + obj + ", request, response )");

	//prevent browser caching this XML
	response.setHeader("Cache-Control","no-store");  //HTTP 1.1   
	response.setHeader("Pragma","no-cache"); //HTTP 1.0
	
	//prevents caching at the proxy server     
	response.setDateHeader("Expires", -1);  
	
	try {
	    
	    Stopwatch timer = new Stopwatch();
	    timer.start();
	    
	    String xml    = marshal(obj, mapping);

	    timer.lap();
	    log.debug("XML Marshalling complete in: " + timer.getLapTime());

	    JSONObject json = org.json.XML.toJSONObject(xml);
		
	    timer.lap();
	    log.debug("XML to JSON complete in: " + timer.getLapTime());

	    //convert JSON object to a string
	    String output = json.toString();

	    timer.lap();
	    log.debug("JSON to String complete in: " + timer.getLapTime());

	    //check if a JSONP callback is supplied
	    String callback = getString("callback", request);
	    if (!Utils.isEmpty(callback)) output = callback + "(" + output + ")" ;

	    log.debug("OUTPUT: " + output);

 	    //send output to the browser
	    marshal(output, response, "application/json");
		      
	    timer.stop();
	    log.debug("returnJSON complete in: " + timer.getElapsedTime());

	    return VIEW_NONE;

	} catch (AppException ae) {
	    log.debug("", ae);

	} catch (IOException ioe) {
	    log.debug("", ioe);

	} catch (MappingException me) {
	    log.debug("", me);
            
	} catch (XMLException xe) {
	    log.debug("", xe);

	} catch (JSONException je) {
	    log.debug("", je);

	}

	return VIEW_MAIN;
    }


    //HOOKS -------------------------------------------------------

    /**
     * Called to execute any actions that need to happen after dispatchCommand completes successfully.
     *
     * Extend this method to implement custom functionality (such as logging access, etc)
     */
    protected void doAfterCommand(Object obj, HttpServletRequest request, HttpServletResponse response) {
	//no-op
    }

}
